LeaderboardPanel.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. 'use client';
  2. import { useState, useEffect, useCallback } from 'react';
  3. import { fetchApi } from '@/lib/utils/client';
  4. import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
  5. import { faXmark, faCrown, faCircleUser } from '@fortawesome/free-solid-svg-icons';
  6. import type { LiveLeaderboardResponse, LeaderboardItem } from '@/types/response/channel/leaderboard';
  7. import './leaderboard-panel.scss';
  8. type Props = {
  9. channelSID: string;
  10. open: boolean;
  11. onClose: () => void;
  12. };
  13. export default function LeaderboardPanel({ channelSID, open, onClose }: Props) {
  14. const [data, setData] = useState<LiveLeaderboardResponse|null>(null);
  15. const [loading, setLoading] = useState(false);
  16. const [error, setError] = useState<string|null>(null);
  17. const load = useCallback(async () => {
  18. setLoading(true);
  19. setError(null);
  20. try {
  21. const res = await fetchApi<LiveLeaderboardResponse>(
  22. `/api/channel/${channelSID}/live-leaderboard`,
  23. { silent: true }
  24. );
  25. if (res.data) {
  26. setData(res.data);
  27. } else {
  28. setError('리더보드를 불러오지 못했습니다.');
  29. }
  30. } catch {
  31. setError('리더보드를 불러오지 못했습니다.');
  32. } finally {
  33. setLoading(false);
  34. }
  35. }, [channelSID]);
  36. useEffect(() => {
  37. if (!open) {
  38. return;
  39. }
  40. load();
  41. const interval = setInterval(load, 15000); // 15초마다 새로고침
  42. return () => clearInterval(interval);
  43. }, [open, load]);
  44. if (!open) {
  45. return null;
  46. }
  47. const renderItem = (item: LeaderboardItem) => {
  48. const isCrown = item.rank >= 1 && item.rank <= 3;
  49. const badgeUrl = item.titleIconUrl || item.gradeImageUrl || item.memberIcon;
  50. const rankClass = isCrown ? `leaderboard-panel__rank--top${item.rank}` : '';
  51. return (
  52. <li key={item.memberID} className='leaderboard-panel__item'>
  53. <div className={`leaderboard-panel__rank ${rankClass}`}>
  54. {isCrown ? <FontAwesomeIcon icon={faCrown} /> : item.rank}
  55. </div>
  56. <div className='leaderboard-panel__avatar'>
  57. {badgeUrl ? (
  58. <img src={badgeUrl} alt='' />
  59. ) : (
  60. <FontAwesomeIcon icon={faCircleUser} className='leaderboard-panel__avatar-fallback' />
  61. )}
  62. </div>
  63. <div className='leaderboard-panel__name'>
  64. <span className='leaderboard-panel__name-text'>{item.memberName}</span>
  65. <span className='leaderboard-panel__sid'>@{item.memberSID}</span>
  66. </div>
  67. <div className='leaderboard-panel__xp'>{item.xp.toLocaleString()}XP</div>
  68. </li>
  69. );
  70. };
  71. return (
  72. <div className='leaderboard-panel' role='dialog' aria-modal='true' aria-labelledby='leaderboard-title'>
  73. <div className='leaderboard-panel__overlay' onClick={onClose} />
  74. <div className='leaderboard-panel__box'>
  75. <div className='leaderboard-panel__header'>
  76. <button type='button' className='leaderboard-panel__back' onClick={onClose} aria-label='닫기'>
  77. <FontAwesomeIcon icon={faXmark} />
  78. </button>
  79. <h3 id='leaderboard-title' className='leaderboard-panel__title'>열성 팬</h3>
  80. <span className='leaderboard-panel__count'>{data?.total ?? 0}명</span>
  81. </div>
  82. <div className='leaderboard-panel__body'>
  83. {loading && data === null && <div className='leaderboard-panel__loading'>불러오는 중...</div>}
  84. {error && <div className='leaderboard-panel__error'>{error}</div>}
  85. {data && data.broadcastSessionID === null && (
  86. <div className='leaderboard-panel__empty'>
  87. 현재 진행 중인 방송 세션이 없습니다.
  88. </div>
  89. )}
  90. {data && data.broadcastSessionID !== null && data.list.length === 0 && (
  91. <div className='leaderboard-panel__empty'>
  92. 아직 리더보드 참여자가 없습니다.
  93. </div>
  94. )}
  95. {data && data.list.length > 0 && (
  96. <ul className='leaderboard-panel__list'>
  97. {data.list.map(renderItem)}
  98. </ul>
  99. )}
  100. </div>
  101. {data?.myRank && (
  102. <div className='leaderboard-panel__footer'>
  103. {data.myRank.rank === 0 ? (
  104. <p className='leaderboard-panel__my-empty'>
  105. 채팅이나 후원으로 참여해 보세요.
  106. </p>
  107. ) : (
  108. <p className='leaderboard-panel__my'>
  109. 내 순위: <strong>{data.myRank.rank}위</strong> / {data.myRank.xp.toLocaleString()}XP
  110. </p>
  111. )}
  112. </div>
  113. )}
  114. </div>
  115. </div>
  116. );
  117. }